跳到主要内容

Kubernetes Pod 的生命周期

Pod phase(Pod的相位)

Pod 的 status 在信息保存在 PodStatus 中定义,其中有一个 phase 字段。

Pod 的相位(phase)是 Pod 在其生命周期中的简单宏观概述。该阶段并不是对容器或 Pod 的综合汇总,也不是为了做为综合状态机。

提示

在 Kubernetes 中,各个组件都是分布式部署的,围绕着 kube-apiserver 进行通信,那么不同组件之间进行信息同步,就可以通过 status 进行。像 Node 的 status 就记录了该节点的一些状态信息,其他的控制器,就可以通过 status 知道该 Node 的情况,做一些操作,比如节点宕机修复、可分配资源等。

在整个生命周期中,Pod 会出现 5 种状态(相位),分别如下:

  • 挂起(Pending):api server 已经创建了 Pod 资源对象,但它尚未被调度完成或者仍处于下载镜像的过程中
  • 运行中(Running):Pod 已经被调度至某节点,并且所有容器都已经被 kubelet 创建完成
  • 成功(Succeeded):Pod 中的所有容器都已经成功终止并且不会被重启
  • 失败(Failed):所有容器都已经终止,但至少有一个容器终止失败,即容器返回了非0值的退出状态
  • 未知(Unknown):api server 无法正常获取到 Pod 对象的状态信息,通常由网络通信失败所导致

下图是 Pod 的生命周期示意图,从图中可以看到 Pod 状态的变化。

通过 kubectl 创建 Pod 成功后(下面那节),可以通过如下命令看到 Pod 的状态:

# 这里使用了 kubectl 命令行 JSONPATH 模板能力
$ kubectl get pod twocontainers -o=jsonpath='{.status.phase}'
Running

Pod 的生命周期 ⭐

我们一般将 Pod 对象从创建至终的这段时间范围称为 Pod 的生命周期,它主要包含下面的过程:

  • pod 创建过程
  • 运行初始化容器(init container)过程
  • 运行主容器(main container)
    • 容器启动后钩子(post start)、容器终止前钩子(pre stop)
    • 容器的存活性探测(liveness probe)、就绪性探测(readiness probe)
  • pod 终止过程

生命周期如图所示:

pod 的创建过程

  1. 用户通过 kubectl 或其他 api 客户端提交需要创建的 pod 信息给 apiServer
  2. apiServer 开始生成 pod 对象的信息,并将信息存入 etcd,然后返回确认信息至客户端
  3. apiServer 开始反映 etcd 中的 pod 对象的变化,其它组件使用 watch 机制来跟踪检查 apiServer 上的变动
  4. scheduler 发现有新的 pod 对象要创建,开始为 Pod 分配主机并将结果信息更新至 apiServer
  5. node 节点上的 kubelet 发现有 pod 调度过来,尝试调用 docker 启动容器,并将结果回送至 apiServer
  6. apiServer 将接收到的 pod 状态信息存入 etcd 中

pod 的终止过程

  1. 用户向 apiServer 发送删除 pod 对象的命令
  2. apiServer 中的 pod 对象信息会随着时间的推移而更新,在宽限期内(默认30s),pod 被视为 dead
  3. 将 pod 标记为 terminating 状态
  4. kubelet 在监控到 pod 对象转为 terminating 状态的同时启动 pod 关闭过程
  5. 端点控制器监控到 pod 对象的关闭行为时将其从所有匹配到此端点的 service 资源的端点列表中移除
  6. 如果当前 pod 对象定义了 preStop 钩子处理器,则在其标记为 terminating 后即会以同步的方式启动执行
  7. pod 对象中的容器进程收到停止信号
  8. 宽限期结束后,若 pod 中还存在仍在运行的进程,那么 pod 对象会收到立即终止的信号
  9. kubelet 请求 apiServer 将此 pod 资源的宽限期设置为 0 从而完成删除操作,此时 pod 对于用户已不可见

初始化容器阶段

初始化容器是在 pod 的主容器启动之前要运行的容器,主要是做一些主容器的前置工作,它具有两大特征:

  1. 初始化容器必须运行完成直至结束,若某初始化容器运行失败,那么 kubernetes 需要重启它直到成功完成
  2. 初始化容器必须按照定义的顺序执行,当且仅当前一个成功之后,后面的一个才能运行

初始化容器有很多的应用场景,下面列出的是最常见的几个:

  • 提供主容器镜像中不具备的工具程序或自定义代码
  • 初始化容器要先于应用容器串行启动并运行完成,因此可用于延后应用容器的启动直至其依赖的条件得到满足

接下来做一个案例,模拟下面这个需求:

  • 假设要以主容器来运行 Nginx,但是要求在运行 Nginx 之前要能够连接上 MySQL 和 Redis 所在的服务器。
  • 为了简化测试,事先规定好 MySQL 和 Redis 所在的 IP 地址分别为 192.168.18.103 和 192.168.18.104(注意,这两个 IP 都不能 ping 通,因为环境中没有这两个 IP)。

创建 pod-initcontainer.yaml,内容如下:

apiVersion: v1
kind: Pod
metadata:
name: pod-initcontainer
namespace: dev
labels:
user: alsritter
spec:
containers: # 容器配置
- name: nginx
image: nginx:1.17.1
imagePullPolicy: IfNotPresent
ports:
- name: nginx-port
containerPort: 80
protocol: TCP
resources:
limits:
cpu: "2"
memory: "10Gi"
requests:
cpu: "1"
memory: "10Mi"
initContainers: # 初始化容器配置
- name: test-mysql
image: busybox:1.30
command: ["sh","-c","until ping 192.168.18.103 -c 1;do echo waiting for mysql ...;sleep 2;done;"]
securityContext:
privileged: true # 使用特权模式运行容器
- name: test-redis
image: busybox:1.30
command: ["sh","-c","until ping 192.168.18.104 -c 1;do echo waiting for redis ...;sleep 2;done;"]
# 创建pod
$ kubectl create -f pod-initcontainer.yaml
pod/pod-initcontainer created

# 查看pod状态
# 发现pod卡在启动第一个初始化容器过程中,后面的容器不会运行
$ kubectl describe pod pod-initcontainer -n dev
........
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 49s default-scheduler Successfully assigned dev/pod-initcontainer to node1
Normal Pulled 48s kubelet, node1 Container image "busybox:1.30" already present on machine
Normal Created 48s kubelet, node1 Created container test-mysql
Normal Started 48s kubelet, node1 Started container test-mysql

# 动态查看pod
$ kubectl get pods pod-initcontainer -n dev -w
NAME READY STATUS RESTARTS AGE
pod-initcontainer 0/1 Init:0/2 0 15s
pod-initcontainer 0/1 Init:1/2 0 52s
pod-initcontainer 0/1 Init:1/2 0 53s
pod-initcontainer 0/1 PodInitializing 0 89s
pod-initcontainer 1/1 Running 0 90s

接下来,新开一个 shell,为当前服务器(192.168.18.100)新增两个IP,观察Pod的变化:

ifconfig ens33:1 192.168.18.103 netmask 255.255.255.0 up
ifconfig ens33:2 192.168.18.104 netmask 255.255.255.0 up

Pod 的重启策略

Kubernetes 中定义了如下三种重启策略,可以通过 spec.restartPolicy 字段在 Pod 定义中进行设置。

  • Always 表示一直重启,这也是默认的重启策略。Kubelet 会定期查询容器的状态,一旦某个容器处于退出状态,就对其执行重启操作;
  • OnFailure 表示只有在容器异常退出,即退出码不为 0 时,才会对其进行重启操作;
  • Never 表示从不重启;
提示

在 Pod 中设置的重启策略适用于 Pod 内的所有容器。

虽然我们可以设置一些重启策略,确保容器异常退出时可以重启。但是对于运行中的容器,是不是就意味着容器内的服务正常了呢?

比如某些 Java 进程启动速度非常慢,在容器启动阶段其实是无法提供服务的,虽然这个时候该容器是处于运行状态。

再比如,有些服务的进程发生阻塞,导致无法对外提供服务,这个时候容器对外还是显示为运行态。

那么我们该如何解决此类问题呢?有没有一些方法,比如可以通过一些周期性的检查,来确保容器中运行的业务没有任何问题。

所以需要使用到 Pod 的健康检查

Pod 中的健康检查

Kubernetes 中提供了一系列的健康检查,可以定制调用,来帮助解决类似的问题,我们称之为 Probe(探针)。

目前有如下三种 Probe:

1、livenessProbe 可以用来探测容器是否真的在 “运行”,即 “探活”。如果检测失败的话,这个时候 kubelet 就会停掉该容器,容器的后续操作会受到其重启策略的影响。

2、readinessProbe 常常用于指示容器是否可以对外提供正常的服务请求,即 “就绪”,比如 nginx 容器在 reload 配置的时候无法对外提供 HTTP 服务。

3、startupProbe 则可以用于判断容器是否已经启动好,就比如上面提到的容器启动慢的例子。我们可以通过参数,保证有足够长的时间来应对“超长”的启动时间。 如果检测失败的话,同 livenessProbe 的操作。

如果某个 Probe 没有设置的话,我们默认其是成功的。

为了简化一些通用的处理逻辑,Kubernetes 也为这些 Probe 内置了如下三个 Handler:

  • ExecAction 可以在容器内执行 shell 脚本;
  • HTTPGetAction 方便对指定的端口和 IP 地址执行 HTTP Get 请求;
  • TCPSocketAction 可以对指定端口进行 TCP 检查;

在这里 Probe 还提供了其他配置字段,比如 failureThreshold (失败阈值)等,具体参考 配置存活、就绪和启动探针

示例 HTTP GET 探针

将创建一个包含 HTTP GET 存活探针的新 pod

该 pod 的描述文件定义了一个 httpGet 存活探针,该探针告诉 Kubernetes 定期在端口 8080 路径上执行 HTTP GET 请求,以确定该容器是否健康。这些请求在容器运行后立即开始。

可以通过查看 kubectl describe 的内容来了解为什么必须重启容器

可以看到容器现在正在运行,但之前由于错误而终止。

这个退出代码为 137,其有特殊的含义,表示该进程由外部信号终止。数字 137 是两个数字的总和:128+x,其中 x 是终止进程的信号编号。在这个例子中,x 等于 9,即 SIGKILL 的信号编号,意味着这个进程被强行终止。

容器生命周期内的 hook

目前在 Kubernetes 中,有如下两种 hook。

PostStart 可以在容器启动之后就执行。但需要注意的是,此 hook 和容器里的 ENTRYPOINT 命令的执行顺序是不确定的。

PreStop 则在容器被终止之前被执行,是一种阻塞式的方式。执行完成后,Kubelet 才真正开始销毁容器。

同上面的 Probe 一样,hook 也有类似的 Handler:

  • Exec 用来执行 Shell 命令;
  • HTTPGet 可以执行 HTTP 请求。

我们来看个例子:

apiVersion: v1
kind: Pod
metadata:
name: lifecycle-demo
namespace: demo
spec:
containers:
- name: lifecycle-demo-container
image: nginx:1.19
lifecycle:
postStart:
exec:
command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]
preStop:
exec:
command: ["/usr/sbin/nginx","-s","quit"]

可以看出来,我们可以借助 preStop 以优雅的方式停掉 Nginx 服务,从而避免强制停止容器,造成正在处理的请求无法响应。

配置初始化容器 ⭐

在 Kubernetes 中还有一种特殊的容器,即 init 容器。看名字就知道,这个容器工作在正常容器(为了方便区分,我们这里称为应用容器)启动之前,通常用来做一些初始化工作,比如环境检测、OSS 文件下载、工具安装,等等。

应用容器专注于业务处理,其他一些无关的初始化任务就可以放到 init 容器中。这种解耦有利于各自升级,也降低相互依赖。

一个 Pod 中允许有一个或多个 init 容器。init 容器和其他一般的容器非常像,其与众不同的特点主要有:

  • 总是运行到完成,可以理解为一次性的任务,不可以运行常驻型任务,因为会 block 应用容器的启动运行;
  • 顺序启动执行,下一个的 init 容器都必须在上一个运行成功后才可以启动;
  • 禁止使用 readiness/liveness 探针,可以使用 Pod 定义的activeDeadlineSeconds,这其中包含了 Init Container 的启动时间;
  • 禁止使用 lifecycle hook。

来看一个 Init 容器的例子

apiVersion: v1
kind: Pod
metadata:
name: myapp-pod
namespace: demo
labels:
app: myapp
spec:
containers:
- name: myapp-container
image: busybox:1.31
command: ['sh', '-c', 'echo The app is running! && sleep 3600']
initContainers:
- name: init-myservice
image: busybox:1.31
command: ['sh', '-c', 'until nslookup myservice; do echo waiting for myservice; sleep 2; done;']
- name: init-mydb
image: busybox:1.31
command: ['sh', '-c', 'until nslookup mydb; do echo waiting for mydb; sleep 2; done;']

在 myapp-container 启动之前,它会依次启动 init-myservice、init-mydb,分别来检查依赖的服务是否可用。